Skip to content

refactor: QA pass on the skills and agents - #2

Merged
sebastian-iancu merged 4 commits into
mainfrom
refactor/skills-agents-qa
Oct 27, 2026
Merged

sebastian-iancu merged 4 commits into
mainfrom
refactor/skills-agents-qa

Conversation

@sebastian-iancu

@sebastian-iancu sebastian-iancu commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A quality pass over the 8 skills, the 2 agents and the Cursor rule, checked against the plugin-dev skill and agent guidance. Four reviewers read the files and proposed edits. Each edit was checked against the files and the cited sources before it went in.

Less duplication, same rules. About 40 rules were written out in full in two places, usually a skill and its own reference, or two skills. Each now lives in one place, with a pointer in the other. Every citation stays next to its rule.

main this branch
Descriptions (always loaded) 9,027 chars 9,533 chars
Skill and agent bodies 9,420 words 9,154 words
References 26,512 words 25,018 words

Descriptions grow on purpose (+506 chars). They now quote real questions, such as "why is this query slow", "which index should I add" and "is this migration safe to run in production". The evals showed most skills seldom loading from a plain question. All descriptions, the two agents' included, stay under 1,024 characters.

Correctness fixes

  • pgsql-reviewer flagged any pgx statement cache behind a pooler. It now flags only a QueryExecMode the pooler cannot serve, as the skill says.
  • pgsql-reviewer accepted a quoted identifier taken from input. It now requires an allowlist, as the skill does.
  • Both agents had different database rules. Both now allow EXPLAIN ANALYZE only on a SELECT the caller approved, and both name ANALYZE as a write.
  • pgsql-migrations said a clean squawk run still needs a lock timeout. squawk 2.70.0 already flags a missing timeout (run in Docker), so the real gap is the retry after a timeout fires.
  • /pgsql-lint-setup: plpgsql_check reports injection only with security_warnings => true, so the example now passes it.
  • pgsql-query: the isolation retry is SQLSTATE 40001. A deadlock (40P01) can happen at any isolation level.
  • pgsql-indexing and pgsql-performance gave different fixes for created_at::date. Both now say a range on the plain column needs no expression index.
  • Smaller fixes: a wrong cross-reference, an uncited PostgreSQL 18 claim, the fillfactor wording, and one GIN operator-class wording.
  • "(checked)" in rule text now leaves the version and date to ## Sources, as AGENTS.md asks.

Other changes

  • pgsql-plan-analyst now also triggers on a plan the assistant just produced. When no skill base directory is printed (possible on Cursor), it falls back to the summariser path under the plugin root.
  • The Cursor rule now points SQL reviews to pgsql-reviewer.

Not changed

  • A suggestion to narrow the ON CONFLICT arbiter rule to DO UPDATE was turned down: the PostgreSQL 18 manual applies it to every ON CONFLICT.
  • One reviewer's broad "same treatment for the remaining pairs" list named no exact edits, so it is left for a later pass.
  • No CHANGELOG line: every component is still unreleased under [Unreleased] / Added.

Checklist

  • ./scripts/validate.sh, python3 scripts/validate.py --selftest and ./scripts/hooks-test.sh pass
  • claude plugin validate . passes
  • Skill / command / agent triggering tested locally (see docs/testing.md): evals not re-run after the description changes
  • Cursor install tested locally when manifest, rule, or hook content changed (see docs/install.md): the rule gained one section; not loaded in Cursor
  • Both manifests kept in sync when metadata changed: not changed
  • Every new or changed rule cites a source read for this change, and the reference's ## Sources carries the checked date
  • python3 scripts/validate.py --check-links passes when citations changed (255 of 255)
  • Version bumped and CHANGELOG.md updated (if component content changed): not needed, 0.1.0 is unreleased
  • Docs synced (AGENTS.md, README.md, docs/testing.md, the router, the Cursor rule, hooks/session-start.sh) when components were added or renamed: none added or renamed

🤖 Generated with Claude Code

sebastian-iancu and others added 3 commits October 27, 2026 13:49
- pgsql-reviewer: the checklist names each check and points to the skill
  that owns the rule instead of restating it; the pgx cue flags only a
  QueryExecMode the pooler cannot serve; an identifier from input needs
  an allowlist (quoting alone is not enough); squawk rule ids are cited
  only where squawk ran.
- pgsql-plan-analyst: triggers on a plan the assistant just produced;
  falls back to skills/pgsql-performance/scripts/plan-summary.py under
  the plugin root when no skill base directory is printed.
- Both: EXPLAIN ANALYZE only on a SELECT the caller allowed; never DDL,
  DML, ANALYZE, or EXPLAIN ANALYZE of a write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Remove about 40 rules stated in full in two places (a skill and its
  reference, or two skills); the second place keeps a pointer.
- Descriptions quote the questions users ask ("why is this query slow",
  "which index should I add", "is this migration safe to run in
  production"); all stay under 1,024 characters.
- "(checked)" in rule text, the version and date in ## Sources.
- pgsql-migrations: squawk 2.70.0 already flags a missing lock or
  statement timeout; what it cannot see is the retry after a timeout.
- pgsql-lint-setup: plpgsql_check reports injection only with
  security_warnings => true.
- pgsql-query: the isolation retry is SQLSTATE 40001; a deadlock
  (40P01) can happen at any isolation level.
- pgsql-indexing: a range on the plain column needs no expression index
  for created_at::date.
- Smaller fixes: a wrong cross-reference, an uncited PostgreSQL 18 claim,
  the fillfactor wording, a GIN operator-class wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sebastian-iancu sebastian-iancu left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

No critical defects. The correctness fixes check out: a default GIN index is jsonb_ops; UNIQUE NULLS NOT DISTINCT is scoped to one column; plpgsql_check_function is called with security_warnings => true; 40001 is the isolation retry and 40P01 can happen at any level; a range on the bare created_at column needs no expression index. Skill descriptions are third person, quote real questions, and all eight measure under 1,024 characters (957–1,000). scripts/validate.py passes. The .squawk.toml block in pgsql-lint-setup still matches references/squawk.toml. Agent frontmatter is sound: model: inherit, distinct colors, tools: allowlists with no Write/Edit/Agent, and prose "When to invoke" sections.

Important

1. EXPLAIN ANALYZE of a write is forbidden and prescribed in the same change.

Both agents now say: EXPLAIN (ANALYZE, BUFFERS) only on a SELECT the caller allowed; never DDL, DML, ANALYZE, or EXPLAIN ANALYZE of a write.

These still say to wrap the write and roll it back:

  • rules/pgsql-context.mdc ("Running SQL against a database"), which this PR edited
  • skills/pgsql-performance/SKILL.md
  • skills/pgsql-performance/references/explain.md, including a worked UPDATE
  • agents/pgsql-plan-analyst.md edge case "Estimates only": ask for EXPLAIN (ANALYZE, BUFFERS), "run inside BEGIN ... ROLLBACK for a write"

Step 1 of both agents loads pgsql-performance and reads explain.md. Nothing says the agent's operating rule wins over that recipe, and the plan-analyst's own edge case contradicts its operating rule. The skill's recipe is right for a session the user is driving. One clause on the agent side ("ask the caller to run a write inside BEGIN/ROLLBACK; do not run it yourself") and one on the rule ("the two agents do not run it") would close it. "Never ANALYZE" also sits next to step 6's "ANALYZE then the same EXPLAIN" as the caller's next command; "never run ANALYZE" makes that a recommendation rather than a ban on the word.

2. The two agent descriptions are over the 1,024-character cap the PR claims, and CI does not see it.

Measured with frontmatter_value from scripts/validate.py: pgsql-plan-analyst 1,044, pgsql-reviewer 1,148. Every skill is under the cap. The check lives only in validate_skills(), so validate.py stays green. docs/authoring.md states the Agent Skills limit as 1,024 for the always-on description. The reviewer's extra text is the "do not dispatch it beside that reviewer" sentence (already in the body) and then the "Not for" clause and the "When to invoke" pointer, which is the part a truncation would drop. Trim those two descriptions, or teach validate_md_components the same limit and say so in docs/authoring.md.

Suggestions

  • skills/pgsql-indexing/references/design.md: "Column order and skip scan" no longer states column order, and "Foreign keys" no longer says to index the referencing columns. Both rules are in SKILL.md with no pointer back, while "Read when" and the contents list still send those tasks here. "Uniqueness and NULLs" also dropped "nulls are distinct by default"; that sentence now lives only in pgsql-schema/references/constraints.md.
  • skills/pgsql-jsonb/references/functions.md: "JSON_ARRAYAGG needs the same COALESCE as jsonb_agg (SKILL.md)" points at a rule that names only jsonb_agg.
  • skills/pgsql-query/references/go-pgx.md no longer mentions pgx.Identifier.Sanitize(). The skill still states the allowlist rule; php-pdo.md still states the PHP twin. One pointer line would keep the Go reference usable on its own.
  • skills/pgsql-migrations/references/patterns.md, "Add a CHECK or a foreign key": the bullet now starts at "squawk:" with no bold lead. The two-step NOT VALID / VALIDATE rule is in the skill and only in the SQL comments here.
  • pgsql-query's description replaced "retry on serialization failure" with "retry on deadlock". The rule covers both SQLSTATEs, and the description is 1,000 characters, so both phrases fit.
  • pgsql-schema/SKILL.md still shows plpgsql_check_function('fn(int)') with no security_warnings. pgsql-lint-setup and pgsql-schema/references/functions.md now pass security_warnings => true.
  • pgsql-reviewer no longer says to mark a plan claim [NEEDS-CONFIRMATION] when no database was named. The generic edge case still has the marker; the specific trigger does not.

The open checklist items are still open: the description changes were not re-run through the evals, and the new "Reviewing SQL" section of the Cursor rule has not been loaded in Cursor.

Recommendation

Fix the two important items before merge. The suggestions can go in the same commit. I would not block on them alone.

@sebastian-iancu

Copy link
Copy Markdown
Contributor Author

Review addressed in 1c89018. Every item was checked against the branch first; all held.

Important

1. EXPLAIN ANALYZE of a write. Fixed in both agents. The operating rule now reads: never run DDL, DML or ANALYZE, and never EXPLAIN ANALYZE a write, not even inside the BEGIN ... ROLLBACK a skill or rule prescribes; name that command for the caller to run. "Never run ANALYZE" now reads as a ban on running it, so step 6's "ANALYZE then the same EXPLAIN" stays a command for the caller. The plan analyst's "Estimates only" edge case now asks the caller to run it.

Not done: the extra clause in rules/pgsql-context.mdc. The precedence now lives in the agents, which name "a skill or rule" as what they override, so a second statement in the rule would give one rule two homes. The rule's recipe stays correct for the session the user drives.

2. Agent descriptions over 1,024. Fixed: pgsql-plan-analyst is 996 characters and pgsql-reviewer 1,002, measured with frontmatter_value. The reviewer's was already 1,109 on main. No check is lost: the dispatch sentence, the "Not for" clause and the "When to invoke" pointer all stay, with shorter wording. The PR body's figures are corrected (descriptions 9,533 chars, +506).

Not done: a validator check for agents. docs/authoring.md gives 1,024 as the Agent Skills limit for SKILL.md, and I know of no documented limit for agent descriptions. Note that #3 adds Cursor text to the same reviewer sentence: it now conflicts with this branch, and keeping its wording would put the description back over 1,024. If agents should share the cap, a check in validate_md_components would catch that.

Suggestions (all applied)

  • pgsql-indexing/references/design.md: pointer lines restored for column order and foreign-key indexes (to SKILL.md), and for NULLs in a unique index (to pgsql-schema constraints.md).
  • pgsql-jsonb/references/functions.md: the line now states the rule and cites the manual. On PostgreSQL 18.6, JSON_ARRAYAGG(x) IS NULL over zero rows returns t.
  • pgsql-query/references/go-pgx.md: the pgx.Identifier{...}.Sanitize() line is back, with the allowlist pointer.
  • pgsql-migrations/references/patterns.md: the bold lead "Both kinds go NOT VALID first and validate later" is back, with its ALTER TABLE citation.
  • pgsql-query description: "retry on a serialization failure or deadlock". "jsonb operators and functions" is shortened to "jsonb" to make room (1,003 chars).
  • pgsql-schema/SKILL.md: the backstop now passes security_warnings => true.
  • pgsql-reviewer: "mark plan claims [NEEDS-CONFIRMATION]" is back on the no-database line.

Checks: validate.sh, --selftest (19), hooks-test.sh, plan-summary-test.py, claude plugin validate . and --check-links (255 of 255) pass locally.

Still open: the eval re-run and loading the rule in Cursor.

@sebastian-iancu
sebastian-iancu merged commit 765d6c3 into main Oct 27, 2026
2 checks passed
Review of PR #2:
- Both agents: never run ANALYZE, DML or EXPLAIN ANALYZE of a write,
  even inside the BEGIN ... ROLLBACK a skill or rule prescribes; name
  the command for the caller. The plan analyst asks the caller to run
  EXPLAIN ANALYZE; the reviewer marks plan claims [NEEDS-CONFIRMATION]
  again when no database is named.
- Agent descriptions trimmed to 996 and 1,002 characters.
- Skills: pointers back for column order, foreign-key indexes and
  NULLs in unique indexes (indexing design.md); JSON_ARRAYAGG's empty
  input stated with its source (jsonb functions.md, checked on 18.6);
  pgx.Identifier.Sanitize() restored (go-pgx.md); the NOT VALID lead
  restored (migrations patterns.md); plpgsql_check_function with
  security_warnings (schema); "serialization failure" back in the
  pgsql-query description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant